﻿using System;
using System.Collections.Generic;
using System.Linq;
using ErrorDumper.Configuration;

namespace ErrorDumper.Filtering {
    public static class ExceptionFilters {

        private static readonly object _syncRoot = new object();
        private static bool _isLoaded;
        private static ExceptionHandlingLevel _defaultLevel;
        private static IExceptionFilter _rulesFilter;

        public static readonly HashSet<IExceptionFilter> _globalFilters =
            new HashSet<IExceptionFilter>();

        public static readonly Dictionary<Type, HashSet<IExceptionFilter>> _typeFilters =
            new Dictionary<Type, HashSet<IExceptionFilter>>();

        public static void Reset() {
            lock (_syncRoot)
                _isLoaded = false;
        }

        private static void Load() {
            lock (_syncRoot) {
                if (_isLoaded)
                    return;

                _defaultLevel = ExceptionHandlingLevel.WriteExtendedDump;
                _globalFilters.Clear();
                _typeFilters.Clear();
                _rulesFilter = null;

                var section = FilteringConfigurationSection.GetSection();
                if (section != null) {
                    _defaultLevel = section.DefaultLevel;
                    _rulesFilter = section;
                }

                _isLoaded = true;
            }
        }

        public static ExceptionHandlingLevel DefaultLevel {
            get {
                lock (_syncRoot) {
                    if (!_isLoaded)
                        Load();
                    return _defaultLevel;
                }
            }
            set {
                lock (_syncRoot)
                    _defaultLevel = value;
            }
        }

        private static IExceptionFilter RulersFilter {
            get {
                lock (_syncRoot) {
                    if (!_isLoaded)
                        Load();
                    return _rulesFilter;
                }
            }
        }

        #region GlobalFilters

        public static IEnumerable<IExceptionFilter> GetGlobalFilters() {
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _globalFilters.ToArray();
            }
        }

        public static bool RegisterGlobalFilter(IExceptionFilter filter) {
            if (filter == null)
                throw new ArgumentNullException("filter");
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _globalFilters.Add(filter);
            }
        }

        public static bool RegisterGlobalFilter(Type filterType) {
            if (filterType == null)
                throw new ArgumentNullException("filterType");
            if (!typeof(IExceptionFilter).IsAssignableFrom(filterType))
                throw new ArgumentOutOfRangeException("filterType");
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return !_globalFilters.Any(i => i.GetType() == filterType)
                    ? _globalFilters.Add((IExceptionFilter)Activator.CreateInstance(filterType))
                    : false;
            }
        }

        public static bool RegisterGlobalFilter<TFilter>()
            where TFilter : class, IExceptionFilter, new() {
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return !_globalFilters.Any(i => i.GetType() == typeof(TFilter))
                    ? _globalFilters.Add(new TFilter())
                    : false;
            }
        }

        public static bool IsRegisteredGlobalFilter(IExceptionFilter filter) {
            if (filter == null)
                throw new ArgumentNullException("filter");
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _globalFilters.Contains(filter);
            }
        }

        public static bool IsRegisteredGlobalFilter(Func<IExceptionFilter, bool> predicate) {
            if (predicate == null)
                throw new ArgumentNullException("predicate");
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _globalFilters.Any(predicate);
            }
        }

        public static bool IsRegisteredGlobalFilter<TFilter>()
            where TFilter : class, IExceptionFilter, new() {
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _globalFilters.Any(i => i.GetType() == typeof(TFilter));
            }
        }

        public static bool UnregisterGlobalFilter(IExceptionFilter filter) {
            if (filter == null)
                throw new ArgumentNullException("filter");
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _globalFilters.Remove(filter);
            }
        }

        public static int UnregisterGlobalFilter(Type filterType) {
            if (filterType == null)
                throw new ArgumentNullException("filterType");
            if (!typeof(IExceptionFilter).IsAssignableFrom(filterType))
                throw new ArgumentOutOfRangeException("filterType");
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _globalFilters.RemoveWhere(i => i.GetType() == filterType);
            }
        }

        public static int UnregisterGlobalFilter<TFilter>()
            where TFilter : class, IExceptionFilter {
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _globalFilters.RemoveWhere(i => i.GetType() == typeof(TFilter));
            }
        }

        public static int UnregisterGlobalFilter(Func<IExceptionFilter, bool> predicate) {
            if (predicate == null)
                throw new ArgumentNullException("predicate");
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return _globalFilters.RemoveWhere(i => predicate(i));
            }
        }

        #endregion

        #region TypeFilters

        public static IEnumerable<IExceptionFilter> GetTypeFilters(Func<Type, bool> typePredicate) {
            if (typePredicate == null)
                throw new ArgumentNullException("typePredicate");
            
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                return (from entry in _typeFilters
                        where typePredicate(entry.Key)
                        from filter in entry.Value
                        select filter).ToArray();
            }
        }

        private static void ValidateExceptionType(Type exceptionType) {
            if (exceptionType == null)
                throw new ArgumentNullException("exceptionType");
            if (!typeof(Exception).IsAssignableFrom(exceptionType))
                throw new ArgumentOutOfRangeException("exceptionType");
        }

        public static bool RegisterTypeFilter(Type exceptionType, IExceptionFilter filter) {
            ValidateExceptionType(exceptionType);
            if (filter == null)
                throw new ArgumentNullException("filter");
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                HashSet<IExceptionFilter> filters;
                if (!_typeFilters.TryGetValue(exceptionType, out filters))
                    _typeFilters.Add(exceptionType, filters = new HashSet<IExceptionFilter>());
                return filters.Add(filter);
            }
        }

        public static bool RegisterTypeFilter<TException>(IExceptionFilter filter)
            where TException : Exception {
            if (filter == null)
                throw new ArgumentNullException("filter");
            return RegisterTypeFilter(typeof(TException), filter);
        }

        public static bool RegisterTypeFilter<TException, TFilter>()
            where TException : Exception
            where TFilter : class, IExceptionFilter, new() {
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                HashSet<IExceptionFilter> filters;
                if (!_typeFilters.TryGetValue(typeof(TException), out filters))
                    _typeFilters.Add(typeof(TException), filters = new HashSet<IExceptionFilter>());
                return !filters.Any(i => i.GetType() == typeof(TFilter))
                    ? filters.Add(new TFilter())
                    : false;
            }
        }

        public static bool IsRegisteredTypeFilter(
            Type exceptionType, Func<IExceptionFilter, bool> predicate) {
            if (predicate == null)
                throw new ArgumentNullException("predicate");
            ValidateExceptionType(exceptionType);
            lock (_syncRoot) {
                if (!_isLoaded)
                    Load();
                HashSet<IExceptionFilter> filters;
                if (!_typeFilters.TryGetValue(exceptionType, out filters))
                    return false;
                return filters.Any(predicate);
            }
        }

        public static bool IsRegisteredTypeFilter<TException>(
            Func<IExceptionFilter, bool> predicate)
            where TException : Exception {
            if (predicate == null)
                throw new ArgumentNullException("predicate");
            return IsRegisteredTypeFilter(typeof(TException), predicate);
        }

        #endregion

        internal static bool Resolve(
            ref ExceptionHandlingLevel level, ExceptionHandlingLevel? arg) {
            if (arg.HasValue)
                level = (ExceptionHandlingLevel)Math.Min((byte)level, (byte)arg.Value);
            return level != ExceptionHandlingLevel.Ignore;
        }

        private static ExceptionHandlingLevel GetLevelCore(
            Exception exception, Func<IExceptionFilter, ExceptionHandlingLevel?> action) {

            var level = DefaultLevel;
            if (level == ExceptionHandlingLevel.Ignore)
                return ExceptionHandlingLevel.Ignore;

            foreach (var filter in GetGlobalFilters())
                if (!Resolve(ref level, action(filter)))
                    return ExceptionHandlingLevel.Ignore;

            if (exception != null)
                foreach (var filter in GetTypeFilters(t => t.IsInstanceOfType(exception)))
                    if (!Resolve(ref level, action(filter)))
                        return ExceptionHandlingLevel.Ignore;

            var rulersFilter = RulersFilter;
            if (rulersFilter != null)
                Resolve(ref level, action(rulersFilter));

            return level;
        }

        public static ExceptionHandlingLevel GetLevel(
            ExceptionPropertyCollection properties, Exception exception) {
            return GetLevelCore(exception,
                filter => filter.GetLevel(properties, exception));
        }

        public static ExceptionHandlingLevel GetLevel(
            ExceptionPropertyCollection properties, string content) {
            return GetLevelCore(null,
                filter => filter.GetLevel(properties, content));
        }
    }
}